/*
 * Copyright (c) 2008, 2010, 2011
 *      Toni Spets <toni.spets@iki.fi>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "common.h"
#include <sys/select.h>

#include <unistd.h>
#include <sys/types.h>

char cncnet_game_str[4][8] =
{
    "UNK",
    "C&C95",
    "RA1",
    "TS"
};

#define MAX_CLIENTS 64

/* how many ports per ip can be reserved */
#define IPLIMIT 4

/* client considered idle */
#define SOFT_TIMEOUT 5
/* client disconnected */
#define HARD_TIMEOUT 3600

cncnet_client clients[MAX_CLIENTS];
uint32_t boot = 0;
uint32_t now = 0;
int sock = 0;

void jsonp_status(FILE *fh, int players_online, int players_idle, int players[5])
{
    char buf[256] = { 0 };
    snprintf(buf, 256, "cncnet_status({ online: %d, idle: %d, max: %d, games: { cnc95: %d, ra1: %d, ts: %d, ra2: %d }, booted: %d, updated: %d });",
                players_online, players_idle, MAX_CLIENTS, players[GAME_CNC95], players[GAME_RA1], players[GAME_TS], players[GAME_RA2], boot, now);

    rewind(fh);
    fwrite(buf, strlen(buf), 1, fh);
    ftruncate(fileno(fh), strlen(buf));
    fflush(fh);
}

void reset_clients()
{
    int i;
    for(i=0;i<MAX_CLIENTS;i++)
    {
        memset(&clients[i], 0, sizeof(cncnet_client));
    }
}

void welcome(cncnet_client *client, int fake)
{
    char strtmp[128];
    if(client->game == GAME_CNC95)
    {
        protocol_message(client, "EVA", "Welcome to CnCNet!");
        snprintf(strtmp, sizeof(strtmp), "You @ %s:%d\n", inet_ntoa(*(struct in_addr *)&client->addr.sin_addr.s_addr), ntohs(client->addr.sin_port));
        protocol_message(client, "EVA", strtmp);
        if(fake)
        {
            protocol_message(client, "EVA", "Warn: Port masq in use!");
        }
        else if(ntohs(client->addr.sin_port) != NET_PORT)
        {
            protocol_message(client, "EVA", "Warn: Random port detected!");
        }
    }
    else
    {
        snprintf(strtmp, sizeof(strtmp), "Connected to CnCNet from %s:%d with %s\n", inet_ntoa(*(struct in_addr *)&client->addr.sin_addr.s_addr), ntohs(client->addr.sin_port), cncnet_game_str[client->game]);
        protocol_message(client, "CnCNet", strtmp);

        if(fake)
        {
            protocol_message(client, "CnCNet", "Warning: Port number masquerade in use!");
        }
        else if(ntohs(client->addr.sin_port) != NET_PORT)
        {
            protocol_message(client, "CnCNet", "Warning: Random port detected, check your settings!");
        }

        protocol_message(client, "CnCNet", "Join the CnCNet IRC channel to find more players at\n");
        protocol_message(client, "CnCNet", "#cncnet @ irc.freenode.net or use the web interface from\n");
        protocol_message(client, "CnCNet", "http://cncnet.cnc-comm.com/ & click \"Player Lobby\"");
    }
}

cncnet_client *new_client(struct sockaddr_in *addr, cncnet_game game, int fake)
{
    int i;

    int ip_count = 0;
    int reuse_last_packet = now+1;
    int reuse_index = -1;
    for (i=0;i<MAX_CLIENTS;i++)
    {
        if (clients[i].last_packet && addr->sin_addr.s_addr == clients[i].addr.sin_addr.s_addr)
        {
            if (clients[i].last_packet < reuse_last_packet)
            {
                reuse_last_packet = clients[i].last_packet;
                reuse_index = i;
            }

            ip_count++;

            if (ip_count == IPLIMIT)
            {
                printf("> Reused client %d for %s:%d game=%d port_fake=%d\n", reuse_index, inet_ntoa(*(struct in_addr *)&addr->sin_addr.s_addr), ntohs(addr->sin_port), game, fake);
                memcpy(&clients[reuse_index].addr, addr, sizeof(struct sockaddr_in));
                clients[reuse_index].last_packet = now;
                clients[reuse_index].game = game;

                welcome(&clients[reuse_index], fake);

                return &clients[reuse_index];
            }
        }
    }

    for(i=0;i<MAX_CLIENTS;i++)
    {
        if(!clients[i].last_packet)
        {
            printf("> Created new client %d at %s:%d game=%d port_fake=%d (slot %d/%d)\n", i, inet_ntoa(*(struct in_addr *)&addr->sin_addr.s_addr), ntohs(addr->sin_port), game, fake, ip_count+1, IPLIMIT);
            memcpy(&clients[i].addr, addr, sizeof(struct sockaddr_in));
            clients[i].last_packet = now;
            clients[i].game = game;

            welcome(&clients[i], fake);

            return &clients[i];
        }
    }
    printf("Error: Max clients (%d) reached!\n", MAX_CLIENTS);
    return NULL;
}

/* return pointer to client struct on success, NULL if MAX_CLIENTS is reached */
cncnet_client *get_client(struct sockaddr_in *addr)
{
    int i;
    for(i=0;i<MAX_CLIENTS;i++)
    {
        if(!memcmp(&clients[i].addr, addr, sizeof(struct sockaddr_in)))
        {
            return &clients[i];
        }
    }
    return NULL;
}

/* broadcasts a packet to all clients in lobby */
void broadcast(cncnet_client *from, char *buf, int len)
{
    ra_buffer_t buffer;

    buffer.direct = 0x00;
    buffer.from.ip = from->addr.sin_addr.s_addr;
    buffer.from.port = from->addr.sin_port;

    if (len > sizeof(buffer.data))
    {
        printf("The impossible happened, len is bigger than buffer! %d > %d\n", len, sizeof(buffer.data));
        return;
    }

    memcpy(buffer.data, buf, len);

    from->last_packet = now;

    int i;
    for(i=0;i<MAX_CLIENTS;i++)
    {
        if(clients[i].last_packet && &clients[i] != from && clients[i].game == from->game)
        {
            net_send(sock, (const char *)&buffer, sizeof(buffer.from) + len + 1, &clients[i].addr);
        }
    }
}


void process_packet(ra_buffer_t *buf, int len, struct sockaddr_in *addr)
{
    cncnet_client *cliptr;
    cncnet_game game;

    /* allow port faking */
    int fake = 0;
    if (buf->from.port > 1024)
    {
        addr->sin_port = buf->from.port;
        fake = 1;
    }

    cliptr = get_client(addr);

    if(buf->data[0] == 0x34 && buf->data[1] == 0x12)
    {
        game = GAME_CNC95;
    } 
    else if(buf->data[0] == 0x35 && buf->data[1] == 0x12)
    {
        game = GAME_RA1;
    }
    else if(buf->data[4] == 0x35 && buf->data[5] == 0x12)
    {
        game = GAME_TS;
    }
    else if(buf->data[4] == 0x36 && buf->data[5] == 0x12)
    {
        game = GAME_RA2;
    }
    else
    {
        printf("Ignoring unknown packet from %s:%d\n", inet_ntoa(*(struct in_addr *)&addr->sin_addr.s_addr), ntohs(addr->sin_port));
        if (cliptr)
        {
            memset(cliptr, 0, sizeof(cncnet_client));
        }
        return;
    }

    if(buf->direct)
    {
        printf("Ignoring direct packet from %s:%d\n", inet_ntoa(*(struct in_addr *)&addr->sin_addr.s_addr), ntohs(addr->sin_port));
        if (cliptr)
        {
            memset(cliptr, 0, sizeof(cncnet_client));
        }
        return;
    }

    if (!cliptr)
    {
        cliptr = new_client(addr, game, fake);
    }

    if(cliptr)
    {
        if (cliptr->game != game)
        {
            cliptr->game = game;
            cliptr->last_packet = 0;
        }

        if(cliptr->last_packet < now-SOFT_TIMEOUT)
        {
            welcome(cliptr, fake);
        }

        broadcast(cliptr, (char *)&buf->data, len - sizeof(ra_client_t) - 1);
    }
}

int main(int argc, char **argv)
{
    struct sockaddr_in listen;

    reset_clients();
    net_init();

    sock = net_socket();
    net_address_ex(&listen, INADDR_ANY, NET_PORT);
    net_bind(sock, &listen);

    char status[256] = { 0 };
    FILE *jsonp_fh = NULL;

    if (argc > 1)
    {
        printf("Saving JSONP to \"%s\"\n", argv[1]);
        jsonp_fh = fopen(argv[1], "w");
        if (!jsonp_fh)
        {
            perror(argv[1]);
        }
    }

    boot = time(NULL);

    while(1)
    {
        int i;
        struct sockaddr_in addr;
        int players[5] = { 0, 0, 0, 0, 0 };
        int players_online = 0;
        int players_idle = 0;
        int len = 0;
        struct timeval tv = { 1, 0 };
        fd_set read_fds;
        ra_buffer_t buf;

        FD_ZERO(&read_fds);
        FD_SET(sock, &read_fds);

        if (select(sock + 1, &read_fds, NULL, NULL, &tv) > 0)
        {
            len = net_recv(sock, (char *)&buf, sizeof(ra_buffer_t), &addr);
        }

        now = time(NULL);

        /* clear old status line */
        printf("\r");
        for (i=0;i<strlen(status);i++)
            printf(" ");
        printf("\r");
        fflush(NULL);

        if (len)
        {
            process_packet(&buf, len, &addr);
        }

        for(i=0;i<MAX_CLIENTS;i++)
        {
            cncnet_client *cli = &clients[i];

            if (cli->last_packet && cli->last_packet < now-HARD_TIMEOUT)
            {
                memset(&clients[i], 0, sizeof(cncnet_client));
                continue;
            }

            if (cli->game)
            {
                if (cli->last_packet && cli->last_packet < now-SOFT_TIMEOUT)
                {
                    players_idle++;
                }
                else
                {
                    players[cli->game]++;
                    players_online++;
                }
            }
        }

        sprintf(status, "CnCNet [ %d online, %d idle, total %d/%d ][ C&C95: %d, RA: %d, TS: %d, RA2: %d ]",
                players_online, players_idle, players_online + players_idle, MAX_CLIENTS, players[GAME_CNC95], players[GAME_RA1], players[GAME_TS], players[GAME_RA2]);

        printf("%s", status);
        fflush(NULL);

        if (jsonp_fh)
        {
            jsonp_status(jsonp_fh, players_online, players_idle, players);
        }
    }

    return 0;
}
